#region Usings

using System;
using System.Collections;
using System.Linq;
using System.Text;
using Castle.ActiveRecord;
using Castle.Components.Validator;
using log4net;
using NHibernate;
using Karma.Framework.Core.Domain;

#endregion

namespace Karma.Framework.Core.Repository
{
    public class RepositoryBase<T> : IRepository<T> where T : DomainObject
    {
        private static readonly ILog LOG = LogManager.GetLogger(typeof (RepositoryBase<T>).Name);

        #region IQueryable<T> Members

        /// <summary>
        /// Permite realizar consultas sobre un objeto de dominio
        /// </summary>
        /// <param name="specification">Contiene la especificacion de la busqueda a realizar.</param>
        /// <returns>El conjunto de resultados de la busqueda realizada en un objeto de tipo
        /// <see cref="QueryResult"/></returns>
        public Generic.QueryResult<T1> Find<T1>(QuerySpecification specification) where T1 : DomainObject
        {
            var results = InternalFind(specification);
            var count = (Int64)((IList)results[0])[0];
            var result = (IList)results[1];
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Total results (count): {0}", count));
                LOG.Debug(string.Format("Query executed, results retrieved: {0}", result));
            }
            return new Generic.QueryResult<T1>
            {
                Count = count,
                Data = result.Cast<T1>().ToArray()
            };
        }

        /// <summary>
        /// Permite realizar consultas sobre un objeto de dominio
        /// </summary>
        /// <param name="specification">Contiene la especificacion de la busqueda a realizar.</param>
        /// <returns>El conjunto de resultados de la busqueda realizada en un objeto de tipo
        /// <see cref="QueryResult"/></returns>
        public QueryResult Find(QuerySpecification specification)
        {
            var results = InternalFind(specification);
            var count = (Int64)((IList)results[0])[0];
            var result = (IList)results[1];
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Total results (count): {0}", count));
                LOG.Debug(string.Format("Query executed, results retrieved: {0}", result));
            }
            return new QueryResult
                {
                    Count = count,
                    Data = result.Cast<object>().ToArray()
                };
        }

        #endregion

        #region Private methods

        private IList InternalFind(QuerySpecification specification)
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("QueryString: {0}", specification.Query));
                LOG.Debug(string.Format("Criteria: {0}", specification.Criteria));
                LOG.Debug(string.Format("PageSize: {0}", specification.PageSize));
                LOG.Debug(string.Format("Start: {0}", specification.Start));
                if (specification.Sorting != null)
                {
                    LOG.Debug(string.Format("Sorting: {0}", specification.Sorting));
                }
                if (specification.Parameters != null)
                {
                    LOG.Debug(string.Format("Parameters: {0}", specification.Parameters));
                }
            }

            var countQueryStr = ProcessCountQuery(specification).Replace("?", ":criteria");
            var queryStr = ProcessSelectQuery(specification).Replace("?", ":criteria");

            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Count query: {0}", countQueryStr));
                LOG.Debug(string.Format("Search query: {0}", queryStr));
            }

            return (IList)ActiveRecordMediator.Execute(
                typeof(T), delegate(ISession session, object instance)
                {
                    var multiQuery = session.CreateMultiQuery();
                    multiQuery.SetFlushMode(FlushMode.Never);

                    var countHqlQuery =
                        session.CreateQuery(countQueryStr);
                    multiQuery.Add(countHqlQuery);

                    var selectHqlQuery = session.CreateQuery(queryStr);
                    if (specification.PageSize > 0)
                    {
                        selectHqlQuery.SetFirstResult(specification.Start);
                        selectHqlQuery.SetMaxResults(specification.PageSize);
                    }
                    multiQuery.Add(selectHqlQuery);

                    multiQuery.SetParameter("criteria", "%" + specification.Criteria + "%");
                    if (specification.Parameters != null &&
                        specification.Parameters.Length > 0)
                    {
                        var idx = 0;
                        foreach (var o in specification.Parameters)
                        {
                            multiQuery.SetParameter("param" + idx, o);
                            idx++;
                        }
                    }

                    var result = multiQuery.List();
                    if (LOG.IsDebugEnabled)
                    {
                        LOG.Debug(string.Format("Result from query: {0}", result));
                    }
                    return result;
                }, null);
        }

        private static string ProcessOrderInstructions(QuerySpecification specification)
        {
            var order = new StringBuilder();
            var identifier = ExtractIdentifier(specification.Query).Trim();
            
            if (specification.Sorting != null &&
                specification.Sorting.Length > 0)
            {
                order.Append(" order by ");
                for (var idx = 0; idx < specification.Sorting.Length; idx++)
                {
                    var os = specification.Sorting[idx];
                    if (os.Sort == SortEnum.Asc)
                    {
                        order.Append(identifier).Append(".").Append(os.Field).Append(" ").Append
                            ("asc");
                    }
                    else
                    {
                        order.Append(identifier).Append(".").Append(os.Field).Append(
                            " ").Append("desc");
                    }
                    if (idx < specification.Sorting.Length - 1)
                    {
                        order.Append(", ");
                    }
                }
            }
            return order.ToString();
        }

        private static string ExtractIdentifier(string queryString)
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Extracting identifier from query [{0}].", queryString));
            }
            var startidx = queryString.IndexOf(' ', queryString.IndexOf("from"));
            startidx = queryString.IndexOf(' ', startidx + 1) + 1;
            var len = queryString.IndexOf(' ', startidx);
            len = len == -1 ? queryString.Length - startidx : len - startidx;

            return queryString.Substring(startidx, len);
        }

        private static string ExtractFrom(string thequery)
        {
            var startidx = thequery.IndexOf("from");
            return thequery.Substring(startidx, thequery.Length - startidx);
        }

        private static string ProcessSelectQuery(QuerySpecification specification)
        {
            var order = ProcessOrderInstructions(specification);
            var queryStr = new StringBuilder(specification.Query);

            if (!String.IsNullOrEmpty(specification.SubQuery))
            {
                queryStr.Append(" and ").Append(specification.SubQuery);
            }
            queryStr.Append(" ").Append(order);
            return queryStr.ToString();
        }

        private static string ProcessCountQuery(QuerySpecification specification)
        {
            var identifier = ExtractIdentifier(specification.Query).Trim();
            var from = ExtractFrom(specification.Query).Trim();

            var countQueryStr = new StringBuilder(" select count(distinct ")
                .Append(identifier).Append(".Id) ").Append(from);
            if (!String.IsNullOrEmpty(specification.SubQuery))
            {
                countQueryStr.Append(" and ").Append(specification.SubQuery);
            }
            return countQueryStr.ToString();
        }

        #endregion

        #region Implementation of IRepository<T>

        /// <summary>
        /// Obtiene la isntancia definida dada la llave proporcionada
        /// </summary>
        /// <param name="primaryKey">El identificador de la entidad que se busca.</param>
        /// <returns>La entidad</returns>
        public T Get(Int64 primaryKey)
        {
            return (T)ActiveRecordMediator.Execute(
                           typeof(T), delegate(ISession session, object instance)
                           {
                               return session.Get(typeof(T), primaryKey, LockMode.None);
                           }, null);
        }

        public T GetFirst()
        {
            return ActiveRecordMediator<T>.FindFirst();
        }

        public T[] GetAll()
        {
            return ActiveRecordMediator<T>.FindAll();
        }

        /// <summary>
        /// Elimina una entidad dado su identificador
        /// </summary>
        /// <param name="primaryKey">El identificador de la entidad que se desea eliminar.</param>
        public void Delete(Int64 primaryKey)
        {
            var instance = ActiveRecordMediator<T>.FindByPrimaryKey(primaryKey, false);
            if (instance != null)
            {
                ActiveRecordMediator<T>.Delete(instance);
            }
        }

        /// <summary>
        /// Actualiza los datos en la BD dada la instancia de la entidad
        /// </summary>
        /// <param name="instance">La instancia de la entidad.</param>
        public T Update(T instance)
        {
            ActiveRecordMediator<T>.Update(instance);
            return instance;
        }

        /// <summary>
        /// Guarda una nueva isntancia en la BD.
        /// </summary>
        /// <param name="instance">La instancia a guardar</param>
        public T Save(T instance)
        {
            IValidatorRunner runner = new ValidatorRunner(new CachedValidationRegistry());
            if (runner.IsValid(instance))
            {
                ActiveRecordMediator<T>.Save(instance);
                return instance;
            }
            var sb = new StringBuilder ();
            foreach (var msg in runner.GetErrorSummary(instance).ErrorMessages)
            {
                sb.Append(msg).Append(",");
            }
            throw new BusinessException(sb.ToString());
        }

        public T GetForUpdate(Int64 primaryKey)
        {
            return (T)ActiveRecordMediator.Execute(
                           typeof(T), delegate(ISession session, object instance)
                           {
                               return session.Get(typeof(T), primaryKey, LockMode.Upgrade);
                           }, null);
        }

        #endregion
    }
}